/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

use app_units::Au;
use base::print_tree::PrintTree;
use euclid::default::{Point2D, Rect, Size2D};
use fxhash::FxHashSet;
use serde::Serialize;
use style::animation::AnimationSetKey;
use style::dom::OpaqueNode;
use webrender_api::units;
use webrender_traits::display_list::ScrollSensitivity;

use super::{ContainingBlockManager, Fragment, Tag};
use crate::cell::ArcRefCell;
use crate::display_list::StackingContext;
use crate::flow::CanvasBackground;
use crate::geom::PhysicalRect;

#[derive(Serialize)]
pub struct FragmentTree {
    /// Fragments at the top-level of the tree.
    ///
    /// If the root element has `display: none`, there are zero fragments.
    /// Otherwise, there is at least one:
    ///
    /// * The first fragment is generated by the root element.
    /// * There may be additional fragments generated by positioned boxes
    ///   that have the initial containing block.
    pub(crate) root_fragments: Vec<ArcRefCell<Fragment>>,

    /// The scrollable overflow rectangle for the entire tree
    /// <https://drafts.csswg.org/css-overflow/#scrollable>
    pub(crate) scrollable_overflow: PhysicalRect<Au>,

    /// The containing block used in the layout of this fragment tree.
    pub(crate) initial_containing_block: PhysicalRect<Au>,

    /// <https://drafts.csswg.org/css-backgrounds/#special-backgrounds>
    #[serde(skip)]
    pub(crate) canvas_background: CanvasBackground,

    /// Whether or not the root element is sensitive to scroll input events.
    pub root_scroll_sensitivity: ScrollSensitivity,
}

impl FragmentTree {
    pub(crate) fn build_display_list(
        &self,
        builder: &mut crate::display_list::DisplayListBuilder,
        root_stacking_context: &StackingContext,
    ) {
        // Paint the canvas’ background (if any) before/under everything else
        root_stacking_context.build_canvas_background_display_list(
            builder,
            self,
            &self.initial_containing_block,
        );
        root_stacking_context.build_display_list(builder);
    }

    pub fn print(&self) {
        let mut print_tree = PrintTree::new("Fragment Tree".to_string());
        for fragment in &self.root_fragments {
            fragment.borrow().print(&mut print_tree);
        }
    }

    pub fn scrollable_overflow(&self) -> units::LayoutSize {
        units::LayoutSize::from_untyped(Size2D::new(
            self.scrollable_overflow.size.width.to_f32_px(),
            self.scrollable_overflow.size.height.to_f32_px(),
        ))
    }

    pub(crate) fn find<T>(
        &self,
        mut process_func: impl FnMut(&Fragment, usize, &PhysicalRect<Au>) -> Option<T>,
    ) -> Option<T> {
        let info = ContainingBlockManager {
            for_non_absolute_descendants: &self.initial_containing_block,
            for_absolute_descendants: None,
            for_absolute_and_fixed_descendants: &self.initial_containing_block,
        };
        self.root_fragments
            .iter()
            .find_map(|child| child.borrow().find(&info, 0, &mut process_func))
    }

    pub fn remove_nodes_in_fragment_tree_from_set(&self, set: &mut FxHashSet<AnimationSetKey>) {
        self.find(|fragment, _, _| {
            let tag = fragment.tag()?;
            set.remove(&AnimationSetKey::new(tag.node, tag.pseudo));
            None::<()>
        });
    }

    /// Get the vector of rectangles that surrounds the fragments of the node with the given address.
    /// This function answers the `getClientRects()` query and the union of the rectangles answers
    /// the `getBoundingClientRect()` query.
    ///
    /// TODO: This function is supposed to handle scroll offsets, but that isn't happening at all.
    pub fn get_content_boxes_for_node(&self, requested_node: OpaqueNode) -> Vec<Rect<Au>> {
        let mut content_boxes = Vec::new();
        let tag_to_find = Tag::new(requested_node);
        self.find(|fragment, _, containing_block| {
            if fragment.tag() != Some(tag_to_find) {
                return None::<()>;
            }

            let fragment_relative_rect = match fragment {
                Fragment::Box(fragment) | Fragment::Float(fragment) => fragment.border_rect(),
                Fragment::Positioning(fragment) => fragment.rect,
                Fragment::Text(fragment) => fragment.rect,
                Fragment::AbsoluteOrFixedPositioned(_) |
                Fragment::Image(_) |
                Fragment::IFrame(_) => return None,
            };

            let rect = fragment_relative_rect.translate(containing_block.origin.to_vector());

            content_boxes.push(rect.to_untyped());
            None::<()>
        });
        content_boxes
    }

    pub fn get_border_dimensions_for_node(&self, requested_node: OpaqueNode) -> Rect<i32> {
        let tag_to_find = Tag::new(requested_node);
        self.find(|fragment, _, _containing_block| {
            if fragment.tag() != Some(tag_to_find) {
                return None;
            }

            let rect = match fragment {
                Fragment::Box(fragment) => {
                    // https://drafts.csswg.org/cssom-view/#dom-element-clienttop
                    // " If the element has no associated CSS layout box or if the
                    //   CSS layout box is inline, return zero." For this check we
                    // also explicitly ignore the list item portion of the display
                    // style.
                    if fragment.style.get_box().display.is_inline_flow() {
                        return Some(Rect::zero());
                    }

                    let border = fragment.style.get_border();
                    let padding_rect = fragment.padding_rect();
                    Rect::new(
                        Point2D::new(border.border_left_width, border.border_top_width),
                        Size2D::new(padding_rect.size.width, padding_rect.size.height),
                    )
                },
                Fragment::Positioning(fragment) => fragment.rect.cast_unit(),
                _ => return None,
            };

            let rect = Rect::new(
                Point2D::new(rect.origin.x.to_f32_px(), rect.origin.y.to_f32_px()),
                Size2D::new(rect.size.width.to_f32_px(), rect.size.height.to_f32_px()),
            );
            Some(rect.round().to_i32().to_untyped())
        })
        .unwrap_or_else(Rect::zero)
    }

    pub fn get_scrolling_area_for_viewport(&self) -> PhysicalRect<Au> {
        let mut scroll_area = self.initial_containing_block;
        for fragment in self.root_fragments.iter() {
            scroll_area = fragment
                .borrow()
                .scrolling_area(&self.initial_containing_block)
                .union(&scroll_area);
        }
        scroll_area
    }

    pub fn get_scrolling_area_for_node(&self, requested_node: OpaqueNode) -> PhysicalRect<Au> {
        let tag_to_find = Tag::new(requested_node);
        let scroll_area = self.find(|fragment, _, containing_block| {
            if fragment.tag() == Some(tag_to_find) {
                Some(fragment.scrolling_area(containing_block))
            } else {
                None
            }
        });
        scroll_area.unwrap_or_else(PhysicalRect::<Au>::zero)
    }
}
